package offline

import (
	"encoding/json"
	"fmt"
	"os"
	"os/signal"
	"sort"
	"strconv"
	"strings"
	"syscall"
	"time"

	logging "github.com/ipfs/go-log/v2"

	cli2 "github.com/filecoin-project/lotus/cli"
	"github.com/filecoin-project/lotus/cli/offline/local"

	cid "github.com/ipfs/go-cid"
	"github.com/urfave/cli/v2"
	"golang.org/x/xerrors"

	"github.com/filecoin-project/go-address"
	"github.com/filecoin-project/go-state-types/abi"
	"github.com/filecoin-project/go-state-types/big"

	lapi "github.com/filecoin-project/lotus/api"
	"github.com/filecoin-project/lotus/chain/actors/builtin/miner"
	"github.com/filecoin-project/lotus/chain/messagepool"
	"github.com/filecoin-project/lotus/chain/types"
	"github.com/filecoin-project/lotus/node/config"
)

var log = logging.Logger("cli")

var MpoolCmd = &cli.Command{
	Name:  "mpool",
	Usage: "Manage message pool",
	Subcommands: []*cli.Command{
		mpoolPending,
		mpoolStat,
		mpoolReplaceCmd,
		mpoolFlashCmd,
	},
}

var mpoolStat = &cli.Command{
	Name:  "stat",
	Usage: "print mempool stats",
	Flags: []cli.Flag{
		&cli.BoolFlag{
			Name:  "local",
			Usage: "print stats for addresses in local wallet only",
		},
		&cli.IntFlag{
			Name:  "basefee-lookback",
			Usage: "number of blocks to look back for minimum basefee",
			Value: 60,
		},
	},
	Action: func(cctx *cli.Context) error {
		api, closer, err := cli2.GetFullNodeAPI(cctx)
		if err != nil {
			return err
		}
		defer closer()

		ctx := cli2.ReqContext(cctx)

		ts, err := api.ChainHead(ctx)
		if err != nil {
			return xerrors.Errorf("getting chain head: %w", err)
		}
		currBF := ts.Blocks()[0].ParentBaseFee
		minBF := currBF
		{
			currTs := ts
			for i := 0; i < cctx.Int("basefee-lookback"); i++ {
				currTs, err = api.ChainGetTipSet(ctx, currTs.Parents())
				if err != nil {
					return xerrors.Errorf("walking chain: %w", err)
				}
				if newBF := currTs.Blocks()[0].ParentBaseFee; newBF.LessThan(minBF) {
					minBF = newBF
				}
			}
		}

		var filter map[address.Address]struct{}
		if cctx.Bool("local") {
			filter = map[address.Address]struct{}{}

			addrss, err := api.WalletList(ctx)
			if err != nil {
				return xerrors.Errorf("getting local addresses: %w", err)
			}

			for _, a := range addrss {
				filter[a] = struct{}{}
			}
		}

		msgs, err := api.MpoolPending(ctx, types.EmptyTSK)
		if err != nil {
			return err
		}

		type statBucket struct {
			msgs map[uint64]*types.SignedMessage
		}
		type mpStat struct {
			addr                 string
			past, cur, future    uint64
			belowCurr, belowPast uint64
			gasLimit             big.Int
		}

		buckets := map[address.Address]*statBucket{}
		for _, v := range msgs {
			if filter != nil {
				if _, has := filter[v.Message.From]; !has {
					continue
				}
			}

			bkt, ok := buckets[v.Message.From]
			if !ok {
				bkt = &statBucket{
					msgs: map[uint64]*types.SignedMessage{},
				}
				buckets[v.Message.From] = bkt
			}

			bkt.msgs[v.Message.Nonce] = v
		}

		var out []mpStat

		for a, bkt := range buckets {
			act, err := api.StateGetActor(ctx, a, ts.Key())
			if err != nil {
				fmt.Printf("%s, err: %s\n", a, err)
				continue
			}

			cur := act.Nonce
			for {
				_, ok := bkt.msgs[cur]
				if !ok {
					break
				}
				cur++
			}

			var s mpStat
			s.addr = a.String()
			s.gasLimit = big.Zero()

			for _, m := range bkt.msgs {
				if m.Message.Nonce < act.Nonce {
					s.past++
				} else if m.Message.Nonce > cur {
					s.future++
				} else {
					s.cur++
				}

				if m.Message.GasFeeCap.LessThan(currBF) {
					s.belowCurr++
				}
				if m.Message.GasFeeCap.LessThan(minBF) {
					s.belowPast++
				}

				s.gasLimit = big.Add(s.gasLimit, types.NewInt(uint64(m.Message.GasLimit)))
			}

			out = append(out, s)
		}

		sort.Slice(out, func(i, j int) bool {
			return out[i].addr < out[j].addr
		})

		var total mpStat
		total.gasLimit = big.Zero()

		for _, stat := range out {
			total.past += stat.past
			total.cur += stat.cur
			total.future += stat.future
			total.belowCurr += stat.belowCurr
			total.belowPast += stat.belowPast
			total.gasLimit = big.Add(total.gasLimit, stat.gasLimit)

			fmt.Printf("%s: Nonce past: %d, cur: %d, future: %d; FeeCap cur: %d, min-%d: %d, gasLimit: %s\n", stat.addr, stat.past, stat.cur, stat.future, stat.belowCurr, cctx.Int("basefee-lookback"), stat.belowPast, stat.gasLimit)
		}

		fmt.Println("-----")
		fmt.Printf("total: Nonce past: %d, cur: %d, future: %d; FeeCap cur: %d, min-%d: %d, gasLimit: %s\n", total.past, total.cur, total.future, total.belowCurr, cctx.Int("basefee-lookback"), total.belowPast, total.gasLimit)

		return nil
	},
}

var mpoolPending = &cli.Command{
	Name:  "pending",
	Usage: "Get pending messages",
	Flags: []cli.Flag{
		&cli.BoolFlag{
			Name:  "local",
			Usage: "print pending messages for addresses in local wallet only",
		},
		&cli.BoolFlag{
			Name:  "cids",
			Usage: "only print cids of messages in output",
		},
		&cli.StringFlag{
			Name:  "to",
			Usage: "return messages to a given address",
		},
		&cli.StringFlag{
			Name:  "from",
			Usage: "return messages from a given address",
		},
	},
	Action: func(cctx *cli.Context) error {
		api, closer, err := cli2.GetFullNodeAPI(cctx)
		if err != nil {
			return err
		}
		defer closer()

		ctx := cli2.ReqContext(cctx)

		var toa, froma address.Address
		if tos := cctx.String("to"); tos != "" {
			a, err := address.NewFromString(tos)
			if err != nil {
				return fmt.Errorf("given 'to' address %q was invalid: %w", tos, err)
			}
			toa = a
		}

		if froms := cctx.String("from"); froms != "" {
			a, err := address.NewFromString(froms)
			if err != nil {
				return fmt.Errorf("given 'from' address %q was invalid: %w", froms, err)
			}
			froma = a
		}

		var filter map[address.Address]struct{}
		if cctx.Bool("local") {
			filter = map[address.Address]struct{}{}

			addrss, err := api.WalletList(ctx)
			if err != nil {
				return xerrors.Errorf("getting local addresses: %w", err)
			}

			for _, a := range addrss {
				filter[a] = struct{}{}
			}
		}

		msgs, err := api.MpoolPending(ctx, types.EmptyTSK)
		if err != nil {
			return err
		}

		for _, msg := range msgs {
			if filter != nil {
				if _, has := filter[msg.Message.From]; !has {
					continue
				}
			}

			if toa != address.Undef && msg.Message.To != toa {
				continue
			}
			if froma != address.Undef && msg.Message.From != froma {
				continue
			}

			if cctx.Bool("cids") {
				fmt.Println(msg.Cid())
			} else {
				out, err := json.MarshalIndent(msg, "", "  ")
				if err != nil {
					return err
				}
				fmt.Println(string(out))
			}
		}

		return nil
	},
}

var mpoolReplaceCmd = &cli.Command{
	Name:  "replace",
	Usage: "replace a message in the mempool",
	Flags: []cli.Flag{
		&cli.StringFlag{
			Name:  "gas-feecap",
			Usage: "gas feecap for new message (burn and pay to miner, attoFIL/GasUnit)",
		},
		&cli.StringFlag{
			Name:  "gas-premium",
			Usage: "gas price for new message (pay to miner, attoFIL/GasUnit)",
		},
		&cli.Int64Flag{
			Name:  "gas-limit",
			Usage: "gas limit for new message (GasUnit)",
		},
		&cli.BoolFlag{
			Name:  "auto",
			Usage: "automatically reprice the specified message",
		},
		&cli.StringFlag{
			Name:  "max-fee",
			Usage: "Spend up to X attoFIL for this message (applicable for auto mode)",
		},
		commonFlag[0],
		commonFlag[1],
		commonFlag[2],
	},
	ArgsUsage: "<from nonce> | <message-cid>",
	Action: func(cctx *cli.Context) error {

		api, closer, err := cli2.GetFullNodeAPI(cctx)
		if err != nil {
			return err
		}
		defer closer()

		ctx := cli2.ReqContext(cctx)

		var from address.Address
		var nonce uint64
		switch cctx.Args().Len() {
		case 1:
			mcid, err := cid.Decode(cctx.Args().First())
			if err != nil {
				return err
			}

			msg, err := api.ChainGetMessage(ctx, mcid)
			if err != nil {
				return fmt.Errorf("could not find referenced message: %w", err)
			}

			from = msg.From
			nonce = msg.Nonce
		case 2:
			f, err := address.NewFromString(cctx.Args().Get(0))
			if err != nil {
				return err
			}

			n, err := strconv.ParseUint(cctx.Args().Get(1), 10, 64)
			if err != nil {
				return err
			}

			from = f
			nonce = n
		default:
			return cli.ShowCommandHelp(cctx, cctx.Command.Name)
		}

		ts, err := api.ChainHead(ctx)
		if err != nil {
			return xerrors.Errorf("getting chain head: %w", err)
		}

		pending, err := api.MpoolPending(ctx, ts.Key())
		if err != nil {
			return err
		}

		var found *types.SignedMessage
		for _, p := range pending {
			if p.Message.From == from && p.Message.Nonce == nonce {
				found = p
				break
			}
		}

		if found == nil {
			return fmt.Errorf("no pending message found from %s with nonce %d", from, nonce)
		}

		msg := found.Message

		if cctx.Bool("auto") {
			minRBF := messagepool.ComputeMinRBF(msg.GasPremium)

			var mss *lapi.MessageSendSpec
			if cctx.IsSet("max-fee") {
				maxFee, err := types.BigFromString(cctx.String("max-fee"))
				if err != nil {
					return fmt.Errorf("parsing max-spend: %w", err)
				}
				mss = &lapi.MessageSendSpec{
					MaxFee: maxFee,
				}
			}

			// msg.GasLimit = 0 // TODO: need to fix the way we estimate gas limits to account for the messages already being in the mempool
			msg.GasFeeCap = abi.NewTokenAmount(0)
			msg.GasPremium = abi.NewTokenAmount(0)
			retm, err := api.GasEstimateMessageGas(ctx, &msg, mss, types.EmptyTSK)
			if err != nil {
				return fmt.Errorf("failed to estimate gas values: %w", err)
			}

			msg.GasPremium = big.Max(retm.GasPremium, minRBF)
			msg.GasFeeCap = big.Max(retm.GasFeeCap, msg.GasPremium)

			mff := func() (abi.TokenAmount, error) {
				return abi.TokenAmount(config.DefaultDefaultMaxFee), nil
			}

			messagepool.CapGasFee(mff, &msg, mss)
		} else {
			if cctx.IsSet("gas-limit") {
				msg.GasLimit = cctx.Int64("gas-limit")
			}
			msg.GasPremium, err = types.BigFromString(cctx.String("gas-premium"))
			if err != nil {
				return fmt.Errorf("parsing gas-premium: %w", err)
			}
			// TODO: estimate fee cap here
			msg.GasFeeCap, err = types.BigFromString(cctx.String("gas-feecap"))
			if err != nil {
				return fmt.Errorf("parsing gas-feecap: %w", err)
			}
		}

		sm, err := local.OfflineMessagex(cctx, ctx, cctx.String("local-private-key"), msg.Nonce, api, &msg)
		if err != nil {
			return err
		}
		fmt.Println("new message cid: ", sm.Cid())
		return nil
	},
}

var mpoolFlashCmd = &cli.Command{
	Name:  "flash",
	Usage: "flash a message in the mempool",
	Flags: []cli.Flag{
		&cli.StringFlag{
			Name:  "gas-feecap",
			Usage: "gas feecap for new message",
			Value: "4852441321",
		},
		&cli.BoolFlag{
			Name:  "use-gas-feecap",
			Usage: "whether use gas feecap for new message",
		},
		&cli.Int64Flag{
			Name:  "gas-limit",
			Usage: "gas price for new message",
			Value: 65544925,
		},
		&cli.Int64Flag{
			Name:  "gas-base-fee-diff",
			Usage: "gas base fee diff",
			Value: 252441321,
		},
		&cli.Int64Flag{
			Name:  "gas-premium-max",
			Usage: "gas premium max",
			Value: 1013741,
		},
		&cli.Uint64Flag{
			Name:  "pre-commit-sector",
			Usage: "pre commit sectors",
			Value: 6,
		},
		&cli.Uint64Flag{
			Name:  "prove-commit-sector",
			Usage: "prove commit sectors",
			Value: 7,
		},
	},
	ArgsUsage: "<from nonce> | <message-cid>",
	Action: func(cctx *cli.Context) error {
		api, closer, err := cli2.GetFullNodeAPI(cctx)
		if err != nil {
			return err
		}
		defer closer()

		ctx := cli2.ReqContext(cctx)
		gasFeeCap, err := types.BigFromString(cctx.String("gas-feecap"))
		if err != nil {
			fmt.Printf("big from string failed :%+v\n", err)
			return err
		}
		gasbasefeediff, err := types.BigFromString(cctx.String("gas-base-fee-diff"))
		if err != nil {
			fmt.Printf("big from string failed :%+v\n", err)
			return err
		}
		gasPremiumMax, err := types.BigFromString(cctx.String("gas-premium-max"))
		if err != nil {
			fmt.Printf("big from string failed :%+v\n", err)
			return err
		}
		preCommitSector := cctx.Uint64("pre-commit-sector")
		proveCommitSector := cctx.Uint64("prove-commit-sector")
		useGasFeeCap := cctx.Bool("use-gas-feecap")
		from, err := address.NewFromString(cctx.Args().Get(0))
		if err != nil {
			return err
		}
		sigChan := make(chan os.Signal, 2)
		signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT)
		go func() {
			count := 0
			for {
				fmt.Printf("Retry:%d\n", count)
				fmt.Println("start get chain head")
				ts, err := api.ChainHead(ctx)
				if err != nil {
					log.Errorf("getting chain head: %w", err)
					continue
				}
				fmt.Println("start get mpool pending message")
				pending, err := api.MpoolPending(ctx, ts.Key())
				if err != nil {
					fmt.Println("from mpool get message failed")
					time.Sleep(10 * time.Second)
					continue
				}
				fmt.Printf("mpool pending len(%d)\n", len(pending))

				for _, p := range pending {
					msg := p.Message
					if msg.From == from &&
						((msg.Method == miner.Methods.PreCommitSector && preCommitSector == uint64(msg.Method)) ||
							(msg.Method == miner.Methods.ProveCommitSector && proveCommitSector == uint64(msg.Method))) {
						fmt.Printf("msg from:%+v,method:%+v,nonce:%+v\n", msg.From, msg.Method, msg.Nonce)

						for {
							if useGasFeeCap {
								msg.GasFeeCap = gasFeeCap
								time.Sleep(1 * time.Second)
								break
							}
							ts, err := api.ChainHead(ctx)
							if err != nil {
								log.Errorf("getting chain head: %w", err)
								continue
							}
							basefee := ts.MinTicketBlock().ParentBaseFee

							if err != nil {
								fmt.Println("atoi base fee failed: %w", err)
								continue
							}
							fmt.Printf("msg Nonce:(%+v) gasfeecap(%d),basefee(%s)\n", msg.Nonce, msg.GasFeeCap, basefee.String())
							if msg.GasFeeCap.GreaterThan(basefee) && basefee.LessThan(gasFeeCap) {
								msg.GasFeeCap = big.Add(basefee, gasbasefeediff)
								break
							}
							msg.GasFeeCap = gasFeeCap
							time.Sleep(2 * time.Second)
						}

						msg.GasLimit = cctx.Int64("gas-limit")
						if msg.GasPremium.LessThanEqual(gasPremiumMax) {
							msg.GasPremium = big.Add(msg.GasPremium, big.NewInt(10000))
						}

						for i := 0; i < 2; { // 当某条消息的GasPremium设置不合理时，修改GasPremium值后重复一次操作
							if msg.GasPremium.GreaterThanEqual(gasPremiumMax) {
								fmt.Printf("increase GasPremium...skiped gas priemium: %s\n", msg.GasPremium)
								break
							}
							smsg, err := api.WalletSignMessage(ctx, msg.From, &msg)
							if err != nil {
								fmt.Println("failed to sign message: %w", err)
								break
							}
							fmt.Println("push message nonce", msg.Nonce)
							cid, err := api.MpoolPush(ctx, smsg)
							if err == nil {
								fmt.Println("new message cid: ", cid)
								break
							}
							fmt.Printf("push message nonce:%+v done i:%+v \n", msg.Nonce, i)
							errString := err.Error()
							fmt.Println(errString)
							if strings.Contains(errString, "increase GasPremium to") {
								gasPremiumString := getBetweenStr(errString, "increase GasPremium to ", " from", " ")
								msg.GasPremium, err = types.BigFromString(gasPremiumString)
								fmt.Printf("increase GasPremium...%s\n", msg.GasPremium)
								if err != nil {
									fmt.Printf("parsing gas-premium: %s\n", err.Error())
									break
								}
							}
							i++
							time.Sleep(2 * time.Second)
						}
						time.Sleep(3 * time.Second)
					}
				}
				time.Sleep(90 * time.Second)
				count++
			}
		}()
		select {
		case sig := <-sigChan:
			log.Warnw("received shutdown", "signal", sig)
			log.Warn("Shutting down...")
			log.Warn("Graceful shutdown successful")
			os.Exit(0)
		}
		return nil
	},
}

func getBetweenStr(str, start, end1, end2 string) string {
	n := strings.Index(str, start)
	if n == -1 {
		n = 0
	} else {
		n = n + len(start) // 增加了else，不加会把start带上
	}
	str = string([]byte(str)[n:])
	x := strings.Index(str, end1)
	if x != -1 {
		str = string([]byte(str)[:x])
		return str
	}

	// 上面查找第一个结束的字符没找到，下面就查找第二个结束的字符，找不到就返回余下的所有字符
	y := strings.Index(str, end2)
	if y == -1 {
		y = len(str)
	}
	str = string([]byte(str)[:y])
	return str
}
